﻿#region Copyright Notice
/*
 * Copyright (c) 2007 Nick Gravelyn
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 
 * sell copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software. 
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 
 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using EvoGame.Core.Components;
using EvoGame.Core.Interfaces;

namespace EvoGame.Core.Files
{
	public static partial class FileHandler
	{
		/// <summary>
		/// Represents a configuration file.
		/// </summary>
		public class Config : File, ILoadable, ISaveable
		{
			#region Fields and Properties

			protected override FileStream fileStream
			{
				get { return this.file.Open(FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); }
			}

			Dictionary<string, SettingsGroup> _groups;

			/// <summary>
			/// Gets the groups found in the configuration file.
			/// </summary>
			public Dictionary<string, SettingsGroup> SettingGroups
			{
				get { return _groups; }
			}

			#endregion

			#region Public Constructors

			/// <summary>
			/// Loads a configuration file.
			/// </summary>
			/// <param name="file">The filename where the configuration file can be found.</param>
			public Config(string path)
				: base(path)
			{
				_groups = new Dictionary<string, SettingsGroup>();
				this.Load ();
			}

			#endregion

			#region Adding/Deleting Groups

			/// <summary>
			/// Adds a new settings group to the configuration file.
			/// </summary>
			/// <param name="groupName">The name of the group.</param>
			/// <returns>The newly created SettingsGroup.</returns>
			public SettingsGroup AddSettingsGroup(string groupName)
			{
				if (_groups.ContainsKey(groupName))
					throw new Exception("Group already exists with name '" + groupName + "'");

				SettingsGroup group = new SettingsGroup(groupName);
				_groups.Add(groupName, group);

				return group;
			}

			/// <summary>
			/// Deletes a settings group from the configuration file.
			/// </summary>
			/// <param name="groupName">The name of the group to delete.</param>
			public void DeleteSettingsGroup(string groupName)
			{
				_groups.Remove(groupName);
			}

			#endregion

			#region Loading/Saving

			/// <summary>
			/// Loads the configuration from a file.
			/// </summary>
			public void Load()
			{
				//track line numbers for exceptions
				int lineNumber = 0;

				//groups found
				List<SettingsGroup> groups = new List<SettingsGroup>();
				Dictionary<int, string> comments = new Dictionary<int, string> ();

				//current group information
				string currentGroupName = null;
				List<Setting> settings = null;

				using (StreamReader reader = new StreamReader(fileStream))
				{
					while (!reader.EndOfStream)
					{
						string line = reader.ReadLine();
						lineNumber++;

						//strip out comments
						if (line.Contains("#"))
						{
							if (line.IndexOf ("#") == 0)
							{
								comments.Add (lineNumber, line.TrimStart ('#'));
								continue;
							}

							comments.Add(lineNumber, line.Substring(line.IndexOf("#")).TrimStart('#'));
							line = line.Substring(0, line.IndexOf("#"));
						}

						if (line.Contains (";"))
						{
							if (line.IndexOf (";") == 0)
							{
								comments.Add (lineNumber, line.TrimStart (';'));
								continue;
							}

							comments.Add (lineNumber, line.Substring (line.IndexOf (";")).TrimStart (';'));
							line = line.Substring (0, line.IndexOf (";"));
						}


						//trim off any extra whitespace
						line = line.Trim();

						//try to match a group name
						Match match = Regex.Match(line, "\\[[a-zA-Z\\d\\s]+\\]");

						//found group name
						if (match.Success)
						{
							//if we have a current group we're on, we save it
							if (settings != null && currentGroupName != null)
								groups.Add(new SettingsGroup(currentGroupName, settings));

							//make sure the name exists
							if (match.Value.Length == 2)
							{
								if (EvoApp.Debug)
									EvoApp.Debug.Output = string.Format ("Group must have name (line {0})", lineNumber);
								break;
								//throw new Exception(string.Format("Group must have name (line {0})", lineNumber));
							}

							//set our current group information
							currentGroupName = match.Value.Substring(1, match.Length - 2);
							settings = new List<Setting>();
						}

						//no group name, check for setting with equals sign
						else if (line.Contains("="))
						{
							//split the line
							string[] parts = line.Split('=');

							//if we have any more than 2 parts, we have a problem
							if (parts.Length != 2)
							{
								if (EvoApp.Debug)
									EvoApp.Debug.Output = string.Format ("Settings must be in the format 'name = value' (line {0})", lineNumber);
								break;
								//throw new Exception(string.Format("Settings must be in the format 'name = value' (line {0})", lineNumber));
							}
							//trim off whitespace
							parts[0] = parts[0].Trim();
							parts[1] = parts[1].Trim();

							//figure out if we have an array or not
							bool isArray = false;
							bool inString = false;

							//go through the characters
							foreach (char c in parts[1])
							{
								//any comma not in a string makes us creating an array
								if (c == ',' && !inString)
									isArray = true;

								//flip the inString value each time we hit a quote
								else if (c == '"')
									inString = !inString;
							}

							//if we have an array, we have to trim off whitespace for each item and
							//do some checking for boolean values.
							if (isArray)
							{
								//split our value array
								string[] pieces = parts[1].Split(',');

								//need to build a new string
								StringBuilder builder = new StringBuilder();

								for (int i = 0; i < pieces.Length; i++)
								{
									//trim off whitespace
									string s = pieces[i].Trim();

									//convert to lower case
									string t = s.ToLower();

									//check for any of the true values
									if (t == "on" || t == "yes" || t == "true")
										s = "true";

									//check for any of the false values
									else if (t == "off" || t == "no" || t == "false")
										s = "false";

									//append the value
									builder.Append(s);

									//if we are not on the last value, add a comma
									if (i < pieces.Length - 1)
										builder.Append(",");
								}

								//save the built string as the value
								parts[1] = builder.ToString();
							}

							//if not an array
							else
							{
								//make sure we are not working with a string value
								if (!parts[1].StartsWith("\""))
								{
									//convert to lower
									string t = parts[1].ToLower();

									//check for any of the true values
									if (t == "on" || t == "yes" || t == "true")
										parts[1] = "true";

									//check for any of the false values
									else if (t == "off" || t == "no" || t == "false")
										parts[1] = "false";
								}
							}

							string comms = null;
							if (comments.ContainsKey (lineNumber - 1) || comments.ContainsKey (lineNumber))
							{
								if (comments.ContainsKey (lineNumber - 1) && comments.ContainsKey (lineNumber))
									comms = comments[lineNumber - 1] + "\n" + comments[lineNumber];
								else if (comments.ContainsKey (lineNumber - 1))
									comms = comments[lineNumber - 1];
								else
									comms = comments[lineNumber];
							}

							//add the setting to our list making sure, once again, we have stripped
							//off the whitespace
							settings.Add(new Setting(parts[0].Trim(), parts[1].Trim(), isArray, comms));
						}
					}
				}

				//make sure we save off the last group
				if (settings != null && currentGroupName != null)
					groups.Add(new SettingsGroup(currentGroupName, settings));

				//create our new group dictionary
				_groups = new Dictionary<string, SettingsGroup>();

				//add each group to the dictionary
				foreach (SettingsGroup group in groups)
					_groups.Add(group.Name, group);

				if (EvoApp.Debug)
					EvoApp.Debug.Output = "Config file loaded: " + this.file.FullName;
			}

			/// <summary>
			/// Saves the configuration to a file
			/// </summary>
			public void Save()
			{
				using (StreamWriter writer = new StreamWriter(fileStream))
				{
					foreach (KeyValuePair<string, SettingsGroup> groupValue in _groups)
					{
						writer.WriteLine("[{0}]", groupValue.Key);

						foreach (KeyValuePair<string, Setting> settingValue in groupValue.Value.Settings)
							writer.WriteLine("{0} = {1}", settingValue.Key, settingValue.Value.RawValue);

						writer.WriteLine();
					}
				}
			}

			public override void Close()
			{
				this.fileStream.Close();
			}

			#endregion


			/// <summary>
			/// A group of settings from a configuration file.
			/// </summary>
			public class SettingsGroup
			{
				#region Fields and Properties

				string _name;
				Dictionary<string, Setting> _settings;

				/// <summary>
				/// Gets the name of the group.
				/// </summary>
				public string Name
				{
					get { return _name; }
				}

				/// <summary>
				/// Gets the settings found in the group.
				/// </summary>
				public Dictionary<string, Setting> Settings
				{
					get { return _settings; }
				}

				#endregion

				#region Internal Constructors

				internal SettingsGroup(string name)
				{
					_name = name;
					_settings = new Dictionary<string, Setting>();
				}

				internal SettingsGroup(string name, List<Setting> settings)
				{
					_name = name;
					_settings = new Dictionary<string, Setting>();

					foreach (Setting setting in settings)
						try
						{
							_settings.Add(setting.Name, setting);
						}
						catch
						{
							string message = String.Format("Skipping duplicate entry: {0} ( \"{1}\" )", setting.Name, setting.RawValue);
							MessageBox.Show(message, "Duplicate Setting", MessageBoxButtons.OK, MessageBoxIcon.Information);
						}
				}

				#endregion

				#region Adding Non-Array Settings

				/// <summary>
				/// Adds a setting to the group.
				/// </summary>
				/// <param name="name">The name of the setting.</param>
				/// <param name="value">The value of the setting.</param>
				public void AddSetting(string name, int value)
				{
					addSetting(name, value.ToString(), false);
				}

				/// <summary>
				/// Adds a setting to the group.
				/// </summary>
				/// <param name="name">The name of the setting.</param>
				/// <param name="value">The value of the setting.</param>
				public void AddSetting(string name, float value)
				{
					addSetting(name, value.ToString(), false);
				}

				/// <summary>
				/// Adds a setting to the group.
				/// </summary>
				/// <param name="name">The name of the setting.</param>
				/// <param name="value">The value of the setting.</param>
				public void AddSetting(string name, bool value)
				{
					addSetting(name, value.ToString(), false);
				}

				/// <summary>
				/// Adds a setting to the group.
				/// </summary>
				/// <param name="name">The name of the setting.</param>
				/// <param name="value">The value of the setting.</param>
				public void AddSetting(string name, string value)
				{
					value = assertStringQuotes(value);
					addSetting(name, value, false);
				}

				#endregion

				#region Adding Array Settings

				/// <summary>
				/// Adds a setting to the group.
				/// </summary>
				/// <param name="name">The name of the setting.</param>
				/// <param name="value">The values of the setting.</param>
				public void AddSetting(string name, params int[] values)
				{
					StringBuilder builder = new StringBuilder();

					for (int i = 0; i < values.Length; i++)
					{
						builder.Append(values[i]);
						if (i < values.Length - 1)
							builder.Append(",");
					}

					addSetting(name, builder.ToString(), true);
				}

				/// <summary>
				/// Adds a setting to the group.
				/// </summary>
				/// <param name="name">The name of the setting.</param>
				/// <param name="value">The values of the setting.</param>
				public void AddSetting(string name, params float[] values)
				{
					StringBuilder builder = new StringBuilder();

					for (int i = 0; i < values.Length; i++)
					{
						builder.Append(values[i]);
						if (i < values.Length - 1)
							builder.Append(",");
					}

					addSetting(name, builder.ToString(), true);
				}

				/// <summary>
				/// Adds a setting to the group.
				/// </summary>
				/// <param name="name">The name of the setting.</param>
				/// <param name="value">The values of the setting.</param>
				public void AddSetting(string name, params bool[] values)
				{
					StringBuilder builder = new StringBuilder();

					for (int i = 0; i < values.Length; i++)
					{
						builder.Append(values[i]);
						if (i < values.Length - 1)
							builder.Append(",");
					}

					addSetting(name, builder.ToString(), true);
				}

				/// <summary>
				/// Adds a setting to the group.
				/// </summary>
				/// <param name="name">The name of the setting.</param>
				/// <param name="value">The values of the setting.</param>
				public void AddSetting(string name, params string[] values)
				{
					StringBuilder builder = new StringBuilder();

					for (int i = 0; i < values.Length; i++)
					{
						builder.Append(assertStringQuotes(values[i]));
						if (i < values.Length - 1)
							builder.Append(",");
					}

					addSetting(name, builder.ToString(), true);
				}

				#endregion

				#region Deleting Settings

				/// <summary>
				/// Deletes a setting from the group.
				/// </summary>
				/// <param name="name">The name of the setting to delete.</param>
				public void DeleteSetting(string name)
				{
					_settings.Remove(name);
				}

				#endregion

				#region Private Helpers For Adding

				private static string assertStringQuotes(string value)
				{
					//make sure we have our surrounding quotations
					if (!value.StartsWith("\""))
						value = "\"" + value;
					if (!value.EndsWith("\""))
						value = value + "\"";
					return value;
				}

				private void addSetting(string name, string value, bool isArray)
				{
					_settings.Add(name, new Setting(name, value, isArray));
				}

				#endregion
			}

			/// <summary>
			/// A single setting from a configuration file
			/// </summary>
			public class Setting
			{
				#region Fields and Properties

				string _name;
				string _rawValue;
				string _comments;
				bool _isArray = false;

				/// <summary>
				/// Gets the name of the setting.
				/// </summary>
				public string Name
				{
					get { return _name; }
				}

				/// <summary>
				/// Gets the raw value of the setting.
				/// </summary>
				public string RawValue
				{
					get { return _rawValue; }
				}

				/// <summary>
				/// Gets the comments attached to the setting.
				/// </summary>
				public string Comments
				{
					get { return _comments; }
				}

				/// <summary>
				/// Gets whether or not the setting is an array.
				/// </summary>
				public bool IsArray
				{
					get { return _isArray; }
				}

				#endregion

				#region Internal Constructors

				internal Setting(string name, string value, bool isArray)
				{
					_name = name;
					_rawValue = value;
					_isArray = isArray;
					_comments = null;
				}

				internal Setting (string name, string value, bool isArray, string comments)
				{
					_name = name;
					_rawValue = value;
					_isArray = isArray;
					_comments = comments;
				}

				#endregion

				#region Getting Value

				/// <summary>
				/// Attempts to return the setting's value as an integer.
				/// </summary>
				/// <returns>An integer representation of the value</returns>
				public int GetValueAsInt()
				{
					return int.Parse(RawValue);
				}

				/// <summary>
				/// Attempts to return the setting's value as a float.
				/// </summary>
				/// <returns>A float representation of the value</returns>
				public float GetValueAsFloat()
				{
					return float.Parse(RawValue);
				}

				/// <summary>
				/// Attempts to return the setting's value as a bool.
				/// </summary>
				/// <returns>A bool representation of the value</returns>
				public bool GetValueAsBool()
				{
					return Convert.ToBoolean(int.Parse(RawValue));
				}

				/// <summary>
				/// Attempts to return the setting's value as a string.
				/// </summary>
				/// <returns>A string representation of the value</returns>
				public string GetValueAsString()
				{
					if (!RawValue.StartsWith("\"") || !RawValue.EndsWith("\""))
						return RawValue;

					return RawValue.Substring(1, RawValue.Length - 2);

				}

				/// <summary>
				/// Attempts to return the setting's value as an array of integers.
				/// </summary>
				/// <returns>An integer array representation of the value</returns>
				public int[] GetValueAsIntArray()
				{
					string[] parts = RawValue.Split(',');

					int[] valueParts = new int[parts.Length];

					for (int i = 0; i < parts.Length; i++)
						valueParts[i] = int.Parse(parts[i]);

					return valueParts;
				}

				/// <summary>
				/// Attempts to return the setting's value as an array of floats.
				/// </summary>
				/// <returns>An float array representation of the value</returns>
				public float[] GetValueAsFloatArray()
				{
					string[] parts = RawValue.Split(',');

					float[] valueParts = new float[parts.Length];

					for (int i = 0; i < parts.Length; i++)
						valueParts[i] = float.Parse(parts[i]);

					return valueParts;
				}

				/// <summary>
				/// Attempts to return the setting's value as an array of bools.
				/// </summary>
				/// <returns>An bool array representation of the value</returns>
				public bool[] GetValueAsBoolArray()
				{
					string[] parts = RawValue.Split(',');

					bool[] valueParts = new bool[parts.Length];

					for (int i = 0; i < parts.Length; i++)
						valueParts[i] = bool.Parse(parts[i]);

					return valueParts;
				}

				/// <summary>
				/// Attempts to return the setting's value as an array of strings.
				/// </summary>
				/// <returns>An string array representation of the value</returns>
				public string[] GetValueAsStringArray()
				{
					Match match = Regex.Match(RawValue, "[\\\"][a-zA-Z\\d\\s]*[\\\"][,]*");

					List<string> values = new List<string>();

					while (match.Success)
					{
						string value = match.Value;
						if (value.EndsWith(","))
							value = value.Substring(0, value.Length - 1);

						value = value.Substring(1, value.Length - 2);
						values.Add(value);
						match = match.NextMatch();
					}

					return values.ToArray();
				}

				#endregion

				#region Setting Value

				/// <summary>
				/// Sets the value of the setting.
				/// </summary>
				/// <param name="value">The new value to store.</param>
				public void SetValue(int value)
				{
					_rawValue = value.ToString();
				}

				/// <summary>
				/// Sets the value of the setting.
				/// </summary>
				/// <param name="value">The new value to store.</param>
				public void SetValue(float value)
				{
					_rawValue = value.ToString();
				}

				/// <summary>
				/// Sets the value of the setting.
				/// </summary>
				/// <param name="value">The new value to store.</param>
				public void SetValue(bool value)
				{
					_rawValue = value.ToString();
				}

				/// <summary>
				/// Sets the value of the setting.
				/// </summary>
				/// <param name="value">The new value to store.</param>
				public void SetValue(string value)
				{
					_rawValue = assertStringQuotes(value);
				}

				/// <summary>
				/// Sets the value of the setting.
				/// </summary>
				/// <param name="value">The new values to store.</param>
				public void SetValue(params int[] values)
				{
					StringBuilder builder = new StringBuilder();

					for (int i = 0; i < values.Length; i++)
					{
						builder.Append(values[i]);
						if (i < values.Length - 1)
							builder.Append(",");
					}

					_rawValue = builder.ToString();
				}

				/// <summary>
				/// Sets the value of the setting.
				/// </summary>
				/// <param name="value">The new values to store.</param>
				public void SetValue(params float[] values)
				{
					StringBuilder builder = new StringBuilder();

					for (int i = 0; i < values.Length; i++)
					{
						builder.Append(values[i]);
						if (i < values.Length - 1)
							builder.Append(",");
					}

					_rawValue = builder.ToString();
				}

				/// <summary>
				/// Sets the value of the setting.
				/// </summary>
				/// <param name="value">The new values to store.</param>
				public void SetValue(params bool[] values)
				{
					StringBuilder builder = new StringBuilder();

					for (int i = 0; i < values.Length; i++)
					{
						builder.Append(values[i]);
						if (i < values.Length - 1)
							builder.Append(",");
					}

					_rawValue = builder.ToString();
				}

				/// <summary>
				/// Sets the value of the setting.
				/// </summary>
				/// <param name="value">The new values to store.</param>
				public void SetValue(params string[] values)
				{
					StringBuilder builder = new StringBuilder();

					for (int i = 0; i < values.Length; i++)
					{
						builder.Append(assertStringQuotes(values[i]));
						if (i < values.Length - 1)
							builder.Append(",");
					}

					_rawValue = builder.ToString();
				}

				#endregion

				#region Private Helpers For Setting Value

				private static string assertStringQuotes(string value)
				{
					//make sure we have our surrounding quotations
					if (!value.StartsWith("\""))
						value = "\"" + value;
					if (!value.EndsWith("\"") || value.Length == 1)
						value = value + "\"";
					return value;
				}

				#endregion
			}
		}
	}
}